// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:args/command_runner.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/create.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/globals.dart' as globals;

import '../src/common.dart';
import '../src/context.dart';
import '../src/test_flutter_command_runner.dart';

void main() {
  late Directory tempDir;
  late Directory projectDir;

  setUpAll(() async {
    Cache.disableLocking();
    await _ensureFlutterToolsSnapshot();
  });

  setUp(() {
    tempDir = globals.fs.systemTempDirectory
        .createTempSync('flutter_tools_generated_plugin_registrant_test.');
    projectDir = tempDir.childDirectory('flutter_project');
  });

  tearDown(() {
    tryToDelete(tempDir);
  });

  tearDownAll(() async {
    await _restoreFlutterToolsSnapshot();
  });

  testUsingContext('generated plugin registrant passes analysis', () async {
    await _createProject(projectDir, <String>[]);
    // We need a dependency so the plugin registrant is not completely empty.
    await _editPubspecFile(projectDir, _addDependencyEditor('shared_preferences',
        version: '^2.0.0'));
    // The plugin registrant is created on build...
    await _buildWebProject(projectDir);

    // Find the web_plugin_registrant, now that it lives outside "lib":
    final Directory buildDir = projectDir
        .childDirectory('.dart_tool/flutter_build')
        .listSync()
        .firstWhere((FileSystemEntity entity) => entity is Directory) as Directory;

    // Ensure the file exists, and passes analysis.
    final File registrant = buildDir.childFile('web_plugin_registrant.dart');
    expect(registrant, exists);
    await _analyzeEntity(registrant);

    // Ensure the contents match what we expect for a non-empty plugin registrant.
    final String contents = registrant.readAsStringSync();
    expect(contents, contains('// @dart = 2.13'));
    expect(contents, contains("import 'package:shared_preferences_web/shared_preferences_web.dart';"));
    expect(contents, contains('void registerPlugins([final Registrar? pluginRegistrar]) {'));
    expect(contents, contains('SharedPreferencesPlugin.registerWith(registrar);'));
    expect(contents, contains('registrar.registerMessageHandler();'));
  }, overrides: <Type, Generator>{
    Pub: () => Pub.test(
      fileSystem: globals.fs,
      logger: globals.logger,
      processManager: globals.processManager,
      usage: globals.flutterUsage,
      botDetector: globals.botDetector,
      platform: globals.platform,
      stdio: globals.stdio,
    ),
  });

  testUsingContext('generated plugin registrant passes analysis without null safety', () async {
    await _createProject(projectDir, <String>[]);
    // We need a dependency so the plugin registrant is not completely empty.
    await _editPubspecFile(projectDir,
      _composeEditors(<PubspecEditor>[
        _addDependencyEditor('shared_preferences', version: '^2.0.0'),

        // This turns null safety off
        _setDartSDKVersionEditor('>=2.11.0 <3.0.0'),
      ]));

    // The generated main.dart file has a bunch of stuff that is invalid without null safety, so
    // replace it with a no-op dummy main file. We aren't testing it in this scenario anyway.
    await _replaceMainFile(projectDir, 'void main() {}');

    // The plugin registrant is created on build...
    await _buildWebProject(projectDir);

    // Find the web_plugin_registrant, now that it lives outside "lib":
    final Directory buildDir = projectDir
        .childDirectory('.dart_tool/flutter_build')
        .listSync()
        .firstWhere((FileSystemEntity entity) => entity is Directory) as Directory;

    // Ensure the file exists, and passes analysis.
    final File registrant = buildDir.childFile('web_plugin_registrant.dart');
    expect(registrant, exists);
    await _analyzeEntity(registrant);

    // Ensure the contents match what we expect for a non-empty plugin registrant.
    final String contents = registrant.readAsStringSync();
    expect(contents, contains('// @dart = 2.13'));
    expect(contents, contains("import 'package:shared_preferences_web/shared_preferences_web.dart';"));
    expect(contents, contains('void registerPlugins([final Registrar? pluginRegistrar]) {'));
    expect(contents, contains('SharedPreferencesPlugin.registerWith(registrar);'));
    expect(contents, contains('registrar.registerMessageHandler();'));
  }, overrides: <Type, Generator>{
    Pub: () => Pub.test(
      fileSystem: globals.fs,
      logger: globals.logger,
      processManager: globals.processManager,
      usage: globals.flutterUsage,
      botDetector: globals.botDetector,
      platform: globals.platform,
      stdio: globals.stdio,
    ),
  });


  testUsingContext('(no-op) generated plugin registrant passes analysis', () async {
    await _createProject(projectDir, <String>[]);
    // No dependencies on web plugins this time!
    await _buildWebProject(projectDir);

    // Find the web_plugin_registrant, now that it lives outside "lib":
    final Directory buildDir = projectDir
        .childDirectory('.dart_tool/flutter_build')
        .listSync()
        .firstWhere((FileSystemEntity entity) => entity is Directory) as Directory;

    // Ensure the file exists, and passes analysis.
    final File registrant = buildDir.childFile('web_plugin_registrant.dart');
    expect(registrant, exists);
    await _analyzeEntity(registrant);

    // Ensure the contents match what we expect for an empty (noop) plugin registrant.
    final String contents = registrant.readAsStringSync();
    expect(contents, contains('void registerPlugins() {}'));
  }, overrides: <Type, Generator>{
    Pub: () => Pub.test(
      fileSystem: globals.fs,
      logger: globals.logger,
      processManager: globals.processManager,
      usage: globals.flutterUsage,
      botDetector: globals.botDetector,
      platform: globals.platform,
      stdio: globals.stdio,
    ),
  });

  // See: https://github.com/dart-lang/dart-services/pull/874
  testUsingContext('generated plugin registrant for dartpad is created on pub get', () async {
    await _createProject(projectDir, <String>[]);
    await _editPubspecFile(projectDir,
      _addDependencyEditor('shared_preferences', version: '^2.0.0'));
    // The plugin registrant for dartpad is created on flutter pub get.
    await _doFlutterPubGet(projectDir);

    final File registrant = projectDir
        .childDirectory('.dart_tool/dartpad')
        .childFile('web_plugin_registrant.dart');

    // Ensure the file exists, and passes analysis.
    expect(registrant, exists);
    await _analyzeEntity(registrant);

    // Assert the full build hasn't happened!
    final Directory buildDir = projectDir.childDirectory('.dart_tool/flutter_build');
    expect(buildDir, isNot(exists));
  }, overrides: <Type, Generator>{
    Pub: () => Pub.test(
      fileSystem: globals.fs,
      logger: globals.logger,
      processManager: globals.processManager,
      usage: globals.flutterUsage,
      botDetector: globals.botDetector,
      platform: globals.platform,
      stdio: globals.stdio,
    ),
  });

  testUsingContext(
      'generated plugin registrant ignores lines longer than 80 chars',
      () async {
    await _createProject(projectDir, <String>[]);
    await _addAnalysisOptions(
        projectDir, <String>['lines_longer_than_80_chars']);
    await _createProject(tempDir.childDirectory('test_plugin'), <String>[
      '--template=plugin',
      '--platforms=web',
      '--project-name',
      'test_web_plugin_with_a_purposefully_extremely_long_package_name',
    ]);
    // The line for the test web plugin (`  TestWebPluginWithAPurposefullyExtremelyLongPackageNameWeb.registerWith(registrar);`)
    // exceeds 80 chars.
    // With the above lint rule added, we want to ensure that the `generated_plugin_registrant.dart`
    // file does not fail analysis (this is a regression test - an ignore was
    // added to cover this case).
    await _editPubspecFile(
      projectDir,
      _addDependencyEditor(
        'test_web_plugin_with_a_purposefully_extremely_long_package_name',
        path: '../test_plugin',
      )
    );
    // The plugin registrant is only created after a build...
    await _buildWebProject(projectDir);

    // Find the web_plugin_registrant, now that it lives outside "lib":
    final Directory buildDir = projectDir
        .childDirectory('.dart_tool/flutter_build')
        .listSync()
        .firstWhere((FileSystemEntity entity) => entity is Directory) as Directory;

    expect(
      buildDir.childFile('web_plugin_registrant.dart'),
      exists,
    );
    await _analyzeEntity(buildDir.childFile('web_plugin_registrant.dart'));
  }, overrides: <Type, Generator>{
    Pub: () => Pub.test(
      fileSystem: globals.fs,
      logger: globals.logger,
      processManager: globals.processManager,
      usage: globals.flutterUsage,
      botDetector: globals.botDetector,
      platform: globals.platform,
      stdio: globals.stdio,
    ),
  });
}

Future<void> _ensureFlutterToolsSnapshot() async {
  final String flutterToolsPath = globals.fs.path.absolute(globals.fs.path.join(
    'bin',
    'flutter_tools.dart',
  ));
  final String flutterToolsSnapshotPath = globals.fs.path.absolute(
    globals.fs.path.join(
      '..',
      '..',
      'bin',
      'cache',
      'flutter_tools.snapshot',
    ),
  );
  final String dotPackages = globals.fs.path.absolute(globals.fs.path.join(
    '.dart_tool/package_config.json',
  ));

  final File snapshotFile = globals.fs.file(flutterToolsSnapshotPath);
  if (snapshotFile.existsSync()) {
    snapshotFile.renameSync('$flutterToolsSnapshotPath.bak');
  }

  final List<String> snapshotArgs = <String>[
    '--snapshot=$flutterToolsSnapshotPath',
    '--packages=$dotPackages',
    flutterToolsPath,
  ];
  final ProcessResult snapshotResult = await Process.run(
    '../../bin/cache/dart-sdk/bin/dart',
    snapshotArgs,
  );
  printOnFailure('Output of dart ${snapshotArgs.join(" ")}:');
  printOnFailure(snapshotResult.stdout.toString());
  printOnFailure(snapshotResult.stderr.toString());
  expect(snapshotResult.exitCode, 0);
}

Future<void> _restoreFlutterToolsSnapshot() async {
  final String flutterToolsSnapshotPath = globals.fs.path.absolute(
    globals.fs.path.join(
      '..',
      '..',
      'bin',
      'cache',
      'flutter_tools.snapshot',
    ),
  );

  final File snapshotBackup =
      globals.fs.file('$flutterToolsSnapshotPath.bak');
  if (!snapshotBackup.existsSync()) {
    // No backup to restore.
    return;
  }

  snapshotBackup.renameSync(flutterToolsSnapshotPath);
}

Future<void> _createProject(Directory dir, List<String> createArgs) async {
  Cache.flutterRoot = '../..';
  final CreateCommand command = CreateCommand();
  final CommandRunner<void> runner = createTestCommandRunner(command);
  await runner.run(<String>[
    'create',
    ...createArgs,
    dir.path,
  ]);
}

typedef PubspecEditor = void Function(List<String> pubSpecContents);

Future<void> _editPubspecFile(
  Directory projectDir,
  PubspecEditor editor,
) async {
  final File pubspecYaml = projectDir.childFile('pubspec.yaml');
  expect(pubspecYaml, exists);

  final List<String> lines = await pubspecYaml.readAsLines();
  editor(lines);
  await pubspecYaml.writeAsString(lines.join('\n'));
}

Future<void> _replaceMainFile(Directory projectDir, String fileContents) async {
  final File mainFile = projectDir.childDirectory('lib').childFile('main.dart');
  await mainFile.writeAsString(fileContents);
}

PubspecEditor _addDependencyEditor(String packageToAdd, {String? version, String? path}) {
  assert(version != null || path != null,
      'Need to define a source for the package.');
  assert(version == null || path == null,
      'Cannot only load a package from path or from Pub, not both.');
  void editor(List<String> lines) {
    for (int i = 0; i < lines.length; i++) {
      final String line = lines[i];
      if (line.startsWith('dependencies:')) {
        lines.insert(
            i + 1,
            '  $packageToAdd: ${version ?? '\n'
                '   path: $path'}');
        break;
      }
    }
  }
  return editor;
}

PubspecEditor _setDartSDKVersionEditor(String version) {
  void editor(List<String> lines) {
    for (int i = 0; i < lines.length; i++) {
      final String line = lines[i];
      if (line.startsWith('environment:')) {
        for (i++; i < lines.length; i++) {
          final String innerLine = lines[i];
          final String sdkLine = "  sdk: '$version'";
          if(innerLine.isNotEmpty && !innerLine.startsWith('  ')) {
            lines.insert(i, sdkLine);
            break;
          }
          if(innerLine.startsWith('  sdk:')) {
            lines[i] = sdkLine;
            break;
          }
        }
        break;
      }
    }
  }
  return editor;
}

PubspecEditor _composeEditors(Iterable<PubspecEditor> editors) {
  void composedEditor(List<String> lines) {
    for (final PubspecEditor editor in editors) {
      editor(lines);
    }
  }
  return composedEditor;
}

Future<void> _addAnalysisOptions(
    Directory projectDir, List<String> linterRules) async {
  assert(linterRules.isNotEmpty);

  await projectDir.childFile('analysis_options.yaml').writeAsString('''
linter:
  rules:
${linterRules.map((String rule) => '    - $rule').join('\n')}
  ''');
}

Future<void> _analyzeEntity(FileSystemEntity target) async {
  final String flutterToolsSnapshotPath = globals.fs.path.absolute(
    globals.fs.path.join(
      '..',
      '..',
      'bin',
      'cache',
      'flutter_tools.snapshot',
    ),
  );

  final List<String> args = <String>[
    flutterToolsSnapshotPath,
    'analyze',
    target.path,
  ];

  final ProcessResult exec = await Process.run(
    globals.artifacts!.getHostArtifact(HostArtifact.engineDartBinary).path,
    args,
    workingDirectory: target is Directory ? target.path : target.dirname,
  );
  printOnFailure('Output of flutter analyze:');
  printOnFailure(exec.stdout.toString());
  printOnFailure(exec.stderr.toString());
  expect(exec.exitCode, 0);
}

Future<void> _buildWebProject(Directory workingDir) async {
  return _runFlutterSnapshot(<String>['build', 'web'], workingDir);
}

Future<void> _doFlutterPubGet(Directory workingDir) async {
  return _runFlutterSnapshot(<String>['pub', 'get'], workingDir);
}

// Runs a flutter command from a snapshot build.
// `flutterCommandArgs` are the arguments passed to flutter, like: ['build', 'web']
// to run `flutter build web`.
// `workingDir` is the directory on which the flutter command will be run.
Future<void> _runFlutterSnapshot(List<String> flutterCommandArgs, Directory workingDir) async {
  final String flutterToolsSnapshotPath = globals.fs.path.absolute(
    globals.fs.path.join(
      '..',
      '..',
      'bin',
      'cache',
      'flutter_tools.snapshot',
    ),
  );

  final List<String> args = <String>[
    flutterToolsSnapshotPath,
    ...flutterCommandArgs
  ];

  final ProcessResult exec = await Process.run(
    globals.artifacts!.getHostArtifact(HostArtifact.engineDartBinary).path,
    args,
    workingDirectory: workingDir.path,
  );
  printOnFailure('Output of flutter ${flutterCommandArgs.join(" ")}:');
  printOnFailure(exec.stdout.toString());
  printOnFailure(exec.stderr.toString());
  expect(exec.exitCode, 0);
}
